/*
 *
 * $Id: TextMessageElement.java,v 1.1 2005/05/03 06:38:26 hamada Exp $
 *
 * Copyright (c) 2001 Sun Microsystems, Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *       Sun Microsystems, Inc. for Project JXTA."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA"
 *    must not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact Project JXTA at http://www.jxta.org.
 *
 * 5. Products derived from this software may not be called "JXTA",
 *    nor may "JXTA" appear in their name, without prior written
 *    permission of Sun.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL SUN MICROSYSTEMS OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of Project JXTA.  For more
 * information on Project JXTA, please see
 * <http://www.jxta.org/>.
 *
 * This license is based on the BSD license adopted by the Apache Foundation.
 */

package net.jxta.endpoint;

import java.io.Reader;
import java.io.Writer;
import java.lang.ref.SoftReference;

import java.io.IOException;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import net.jxta.document.MimeMediaType;
import net.jxta.document.TextDocument;
import net.jxta.util.CountingWriter;
import net.jxta.util.DevNullWriter;

/**
 *  An extension of MessageElement for managing elements that are composed of
 *  character text. (as opposed to raw bytes).
 */
public abstract class TextMessageElement extends MessageElement implements TextDocument {

    /**
     *  Log4J Logger
     */
    private static final Logger LOG = Logger.getLogger(TextMessageElement.class.getName());

    /**
     *  cached result of {@link #getCharLength()} operation.
     */
    protected transient long cachedGetCharLength = -1;

    /**
     *  cached result of {@link #getBytes(boolean)} operation.
     */
    protected transient SoftReference cachedGetChars = null;

    /**
     *  Internal constructor for initializaing everything but the data.
     *
     *   @param name Name of the Element. May be the empty string ("") if
     *    the Element is not named.
     *   @param type Type of the Element. null is equivalent to specifying
     *    the type "Application/Octet-stream"
     *   @param sig optional message digest/digital signature elemnent. If
     *    no signature is to be specified, pass null.
     */
    protected TextMessageElement(String name, MimeMediaType type, MessageElement sig) {
        super(name, type, sig);
    }

    /**
     *  {@inheritDoc}
     *
     *  <p/>synchronized for caching purposes.
     */
    public synchronized String toString() {
        String result = null;

        if(null != cachedToString) {
            result = (String) cachedToString.get();

            if (null != result)
                return result;
        }

        if (LOG.isEnabledFor(Level.DEBUG)) {
            LOG.debug("creating toString of " + getClass().getName() + '@' + Integer.toHexString(hashCode()));
        }

        StringBuffer theString = new StringBuffer();

        try {
            Reader asString = getReader();
            char [] characters = new char [256];
            do {
                int res = asString.read(characters);

                if(res < 0) {
                    break;
                }

                theString.append(characters, 0, res);
            } while(true);

            result = theString.toString();

            cachedToString = new SoftReference(result);
            return result;
        } catch (IOException caught) {
            if (LOG.isEnabledFor(Level.ERROR)) {
                LOG.error("Could not generate string for element. ", caught);
            }

            throw new IllegalStateException("Could not generate string for element. " + caught);
        }
    }

    /**
     *  {@inheritDoc}
     *
     *  <p/>We use the "text" extension and leave it to sub-classes to extend
     *  this.
     */
    public String getFileExtension() {
        return "txt";
    }

    /**
     *   Returns the size of the element data in characters
     *
     *  <p/>synchronized for caching purposes.
     *
     *   @return long containing the size of the element data.
     */
    public synchronized long getCharLength() {
        if(cachedGetCharLength >= 0)
            return cachedGetCharLength;

        CountingWriter countChars = new CountingWriter(new DevNullWriter());

        try {
            sendToWriter(countChars);
            cachedGetByteLength = countChars.getCharsWritten();
            return cachedGetByteLength;
        } catch(IOException caught) {
            throw new IllegalStateException("Could not get length of element : " + caught.toString());
        }
    }

    /**
     *  Returns a char array which contains the element data. The char array
     *  returned <b>may be shared amongst all copies of the element</b>,
     *  do not modify it. The <tt>copy</tt> parameter allows you to request a
     *  private, modifiable copy of the element data.
     *
     *  <p/>synchronized for caching purposes.
     *
     *  @return char[] Contents of message element.
     */
    public synchronized char[] getChars(boolean copy) {
        char [] result = null;

        if(null != cachedGetChars) {
            result = (char []) cachedGetChars.get();

            if (null != result)
                if (copy) {
                    char [] theCopy = new char[ result.length ];

                    System.arraycopy(theCopy, 0, result, 0, result.length);
                } else {
                    return result;
                }
        }

        if (LOG.isEnabledFor(Level.DEBUG)) {
            LOG.debug("creating getChars of " + getClass().getName() + '@' + Integer.toHexString(hashCode()) );
        }

        long len = getCharLength();

        if(len > Integer.MAX_VALUE) {
            throw new IllegalStateException("MessageElement is too large to be stored in a char array.");
        }

        result = new char [ (int) len ];

        try {
            Reader reader = getReader();

            int toRead = (int) len;
            int offset = 0;

            do {
                int read = reader.read(result, offset, toRead);

                if(-1 == read) {
                    break;
                }

                toRead -= read;
                offset += read;
            } while(toRead < len);

            if (toRead != 0) {
                IOException failure = new IOException("Unexpected EOF");

                if (LOG.isEnabledFor(Level.WARN))
                    LOG.warn(failure, failure);

                throw failure;
            }
        } catch (IOException caught) {
            IllegalStateException failure = new IllegalStateException("Failed to get bytes of Message Element");
            if (LOG.isEnabledFor(Level.WARN)) {
                LOG.warn(failure, caught);
            }
            throw failure;
        }

        // if this is supposed to be a shared buffer then we can cache it.
        if(!copy) {
            cachedGetChars = new SoftReference(result);
        }

        return result;
    }

    /**
     * {@inheritDoc}
     */
    public abstract Reader getReader() throws IOException;

    /**
     * {@inheritDoc}
     *
     * <p/>This version probably has sub-optimal performance. Sub-classes
     * should override this implementation if possible.
     */
    public void sendToWriter(Writer sendTo) throws IOException {
        copyReaderToWriter(getReader(), sendTo);
    }

    /**
     * Copies a reader to a writer with buffering.
     *
     * @param source The reader to copy from.
     * @param sink The writer to send the data to.
     * @throws IOException if there is a problem copying the data
     */
    private void copyReaderToWriter(Reader source, Writer sink) throws IOException {
        int c;
        char [] buf = new char[4096];

        do {
            c = source.read(buf);

            if(-1 == c) {
                break;
            }

            sink.write(buf,0,c);
        } while(true);
    }
}
